/*
 * AscMagnetMonitorSiteFileImpl.java
 *
 * Created on August 27, 2002, 1:19 PM
 *
 * Copyright (c) 2002 The General Electric Company
 */

package com.ge.healthcare.autosc.processor.magmon;

import java.io.File;
import java.io.IOException;
import java.util.Date;
import java.util.Random;

import com.ge.healthcare.autosc.common.ASCLogger;
import com.ge.healthcare.autosc.common.dbLogger.StepLogger;
import com.ge.healthcare.autosc.common.exceptions.PropertyException;
import com.ge.healthcare.autosc.common.util.ApplicationInfo;
import com.ge.healthcare.autosc.common.util.PropertyConstants;
import com.ge.healthcare.autosc.common.util.dataItem.ASCTextFileDataItem;
import com.ge.healthcare.autosc.common.util.dataItem.IASCTextDataItem;
import com.ge.healthcare.autosc.processor.PrcCodeConstants;
import com.ge.healthcare.autosc.processor.exception.MagnetMonitorSiteException;

/**
 * Implementation for accessing and updating a magnet monitor site where
 *  the sites are represented by a directory tree in a file system.
 *
 * @author Daniel S. Horwitz, Compuware Corp.
 *
 */
public class MagnetMonitorSite implements IMagnetMonitorSite
{
    /**
     * Sites directory, which contains information about all the sites.
     */
    private File theSitesDir = null;

    /**
     * Site directory of the current site.  Null if there is no current site.
     */
    private File currentSiteDir = null;

    /**
     * Name of the file used as the lock file for each site.
     */
    private String lockFileName = null;

    /**
     * Lock file for the current site directory.  The current site directory
     *  is locked while it is in use.
     */
    private File siteLockFile = null;

    /**
     * This indicate if the object is initiated for testing
     */
    private boolean testFlag = false;
    
    /**
     * Construct a magnet monitor site instance.  This uses the PPADS
     *  root property to determine the directory tree containing the
     *  sites directory.
     *
     * @exception AscServiceAbortException If unable to determine the
     *  sites directory or the sites directory does not exist.
     */
    public MagnetMonitorSite() throws MagnetMonitorSiteException {
    	String methodName = "MagmonMonitorSite";
        String ppadsRoot = ApplicationInfo.getInstance().getProperty(PropertyConstants.PPADS_ROOT_PROPERTY, "/ascpdata3/data2/ppads");
        String siteDir = ApplicationInfo.getInstance().getProperty(PropertyConstants.PPADS_SITES_DIR, "sites");
        theSitesDir = new File(ppadsRoot, siteDir);
        ASCLogger.debug(this.getClass(), methodName, "Sites directory: " + theSitesDir.getPath());
        if (!theSitesDir.exists()) {
        	throw new MagnetMonitorSiteException(theSitesDir.getPath() + " does not exist");
        }
        try {
        	lockFileName = ApplicationInfo.getInstance().getRequiredPropertyValue(PropertyConstants.PPADS_SITES_LOCK_FILE);
        } catch (PropertyException pe) {
        	throw new MagnetMonitorSiteException(pe);
        }
    }
    
    /**
     * Construct a magnet monitor site instance.  This uses the PPADS
     *  root property to determine the directory tree containing the
     *  sites directory.
     *
     * @param helper Object providing the utility objects and methods.
     *
     * @exception AscServiceAbortException If unable to determine the
     *  sites directory or the sites directory does not exist.
     */
    public MagnetMonitorSite(boolean testFlag) throws MagnetMonitorSiteException {
    	String methodName = "MagmonMonitorSite";
    	String ppadsRoot = null;
    	String siteDir = null;
    	this.testFlag = testFlag;
		ppadsRoot = getProperty(PropertyConstants.PPADS_ROOT_PROPERTY, "/ascpdata3/data2/ppads");
		siteDir = getProperty(PropertyConstants.PPADS_SITES_DIR, "sites");

    	theSitesDir = new File(ppadsRoot, siteDir);
        ASCLogger.debug(this.getClass(), methodName, "Sites directory: " + theSitesDir.getPath());
        if (!theSitesDir.exists()) {
        	throw new MagnetMonitorSiteException(theSitesDir.getPath() + " does not exist",
        			PrcCodeConstants.PRC_F_SHM_014);
        }
       	lockFileName = getRequiredProperty(PropertyConstants.PPADS_SITES_LOCK_FILE);
    }
    
    /**
     * Select the site to use for subsequent operations.  This only
     *  defines the site, the existence of the site is checked when
     *  used.
     * Because this can be used both to select the initial site and also
     *  to switch sites, it has to handle the case of switching sites.
     *  Therefore, if there is an existing site lock, it is released.
     *
     * @param systemId The system identifier of the site to use for subsequent
     *   operations.  The identifier is assumed to be non-null.
     */
    public void selectSite( String systemId )
    {
        releaseSiteLock();
        currentSiteDir = new File( theSitesDir, systemId );
        StepLogger.addStepLog("Select site: " + currentSiteDir.getPath());
    }

    /**
     * Does the site exist?  This checks whether the site directory exists.
     *
     * @return True if the site exists, false if not.
     *
     * @exception AscServiceAbortException If no site is currently selected.
     */
    public boolean exists() {
    	boolean exist = false;
        if(verifySiteSelected()) {
        	exist = currentSiteDir.exists();
        }
        return exist;
    }

    /**
     * Get the specified data item from the site.
     *
     * @param key The key used to identified the data item.
     *
     * @return The specified data item.
     *
     * @exception AscServiceAbortException If no site is currently selected
     *  or a reserved value is used as the key.
     */
    public IASCTextDataItem get( String key ) throws MagnetMonitorSiteException
    {
        if ( lockFileName.equals( key ) )
        {
            String message = lockFileName +
                                     " is reserved, it may not be used as " +
                                     "the name of a site data item";
            throw new MagnetMonitorSiteException(message);
        }
        if(verifySiteSelected()) {
        	verifySiteLocked();
        } else {
        	throw new MagnetMonitorSiteException("No site is currently selected");
        }
        return new ASCTextFileDataItem( currentSiteDir, key );
    }

    private String getProperty(String key, String defaultValue) {
    	String value = null;
    	if(testFlag) {
    		value = System.getProperty(key, defaultValue);
    	} else {
    		value = ApplicationInfo.getInstance().getProperty(key, defaultValue);
    	}
    	return value;
    }
    
    private String getRequiredProperty(String key) 
		throws MagnetMonitorSiteException {
		String value = null;
		if(!testFlag) {
	    	try {
	    		value = ApplicationInfo.getInstance().getRequiredPropertyValue(key);
	    	} catch (PropertyException pe) {
	    		throw new MagnetMonitorSiteException(pe);
	    	}
		} else {
			value = System.getProperty(key);
			if(key == null) {
				throw new MagnetMonitorSiteException("value for property " + key + "is not defined");
			}
		}
		return value;
	}
    
    /**
     * Verify that the site has been selected.
     *
     * @exception AscServiceAbortException If no site is currently selected.
     */
    private boolean verifySiteSelected() {
        if ( currentSiteDir == null )
        {
        	return false;
        	//throw new MagnetMonitorSiteException("No site currently selected");
        }
        return true;
    }

    /**
     * Called when the garbage collector is going to destroy the object.
     *  This makes sure that the clean up was executed.
     */
    protected void finalize()
    {
        cleanUp();
        try
        {
            super.finalize();
        }
        catch ( Throwable ignoreMe )
        {
        }
    }

    /**
     * Perform any clean-up required.  This should be called when done
     *  with the magnet monitor site.  This typically involves things like
     *  closing files and removing locks.
     */
    public void cleanUp()
    {
        releaseSiteLock();
    }

    /**
     * Verify that the site directory is locked.  If necessary, this waits
     *  until the site can be locked.
     *
     * @exception AscServiceAbortException If unable to get the lock after
     *  an appropriate number of retries, an error is detected getting the
     *  system parameters, or an I/O error is detected.
     */
    private void verifySiteLocked() throws MagnetMonitorSiteException
    {
        if ( siteLockFile == null )
        {
            gainSiteLock();
        }
    }

    /**
     * Gain a lock on the site directory.  This waits until the site can
     *  be locked.
     *
     * @exception AscServiceAbortException If unable to get the lock after
     *  an appropriate number of retries, an error is detected getting the
     *  system parameters, or an I/O error is detected.
     */
    private void gainSiteLock() throws MagnetMonitorSiteException
    {
        File lockFile = new File( currentSiteDir, lockFileName );
        removeStaleLock( lockFile );
        createNewLockFile( lockFile );
        ASCLogger.info("Created site lock file " + lockFile.getPath() );
        siteLockFile = lockFile;
    }

    /**
     * Remove the site's lock file if it is stale.  If the lock file exists
     *  and is older than the specified amount of time, it is assumed that
     *  the creator failed to remove the lock.
     * To reduce the possibility of competition between multiple requests,
     *  the lock is checked twice, with a random delay between checks.
     * <pre>
     *  This reduces the possibility of the following scenario:
     *        Process A                     Process B
     *        Check for stale lock - true
     *                                      Check for stale lock - true
     *        Delay                         Delay
     *        Check for stale lock - true
     *        Process swapped out
     *                                      Check for stale lock - true
     *                                      Delete stale lock
     *                                      Create new lock
     *        Delete lock, thinking it was stale.
     * </pre>
     *
     * @param lockFile Path name to the lock file.
     *
     * @exception AscServiceAbortException If an error is detected getting
     *  a system parameter.
     */
    private void removeStaleLock( File lockFile ) {
        long lastTouchedTime = lockFile.lastModified();
        if ( lastTouchedTime == 0L )
        {
            return;
        }
        final int staleSpanMinutes = 
        	ApplicationInfo.getInstance().getIntegerPropertyValue(PropertyConstants.SITES_LOCKFILE_STALE_SPAN_MIN, "60");
        final long staleSpanMsecs = 60L * 1000L * staleSpanMinutes;
        Date currentTime = new Date();
        final long staleIfEarlier = currentTime.getTime() - staleSpanMsecs;
        if ( lastTouchedTime >= staleIfEarlier )
        {
            return;
        }
        try
        {
            Random randomGen = new Random();
            final int sleepMsecs = 1000 + randomGen.nextInt( 1000 );
            Thread.sleep( sleepMsecs );
        }
        catch ( InterruptedException ignoreMe )
        {
        }
        lastTouchedTime = lockFile.lastModified();
        if ( lastTouchedTime == 0L )
        {
            return;
        }
        if ( lastTouchedTime >= staleIfEarlier )
        {
            return;
        }
        lockFile.delete();
    }

    /**
     * Create a new lock file.  If there currently is a lock file, this
     *  method will retry a specified number of times.
     *
     * @param lockFile Path name to the lock file.
     *
     * @exception AscServiceAbortException If unable to get the lock after
     *  an appropriate number of retries, an error is detected getting the
     *  system parameters, or an I/O error is detected.
     */
    private void createNewLockFile( File lockFile )
        throws MagnetMonitorSiteException
    {
        if ( tryToCreateNewLockFile( lockFile ) )
        {
            return;
        }
        final long sleepMsecs = 
        	ApplicationInfo.getInstance().getLongPropertyValue(PropertyConstants.SITES_LOCKFILE_SLEEP_MSEC, "5000");
        final int maxRetries =
        	ApplicationInfo.getInstance().getIntegerPropertyValue(PropertyConstants.SITES_LOCKFILE_MAX_RETRY, "120");
        for ( int tryNumber = 1; tryNumber <= maxRetries; tryNumber += 1 )
        {
            try
            {
                Thread.sleep( sleepMsecs );
            }
            catch ( InterruptedException ignoreMe )
            {
            }
            if ( tryToCreateNewLockFile( lockFile ) )
            {
                return;
            }
        }
        String message = new StringBuffer().append("Unable to create site lock file ")
                         .append(lockFile).append(" even after ")
                         .append(maxRetries).append(" retries, with a delay of ")
                         .append(sleepMsecs).append(" msecs between attempts").toString();
        throw new MagnetMonitorSiteException(message);
    }

    /**
     * Try to create a new lock file.
     *
     * @param lockFile Path name to the lock file.
     *
     * @return True if a new lock file is created, false if not.  Typically,
     *  the failure to create a new lock file occurs because there is an
     *  existing lock file.
     *
     * @exception AscServiceAbortException If an I/O error is detected.
     */
    private boolean tryToCreateNewLockFile( File lockFile )
        throws MagnetMonitorSiteException
    {
        boolean lockFileCreated = false;
        try
        {
            lockFileCreated = lockFile.createNewFile();
        }
        catch ( IOException ioe )
        {
        	throw new MagnetMonitorSiteException("Error detected creating site lock file " +
                             lockFile, ioe);
        }
        return lockFileCreated;
    }

    /**
     * Release the lock on the site directory.  If this class does not
     * have a lock on the site directory, nothing happens.
     */
    private void releaseSiteLock()
    {
        if ( siteLockFile != null )
        {
            siteLockFile.delete();
            ASCLogger.info(this.getClass(), "releaseSiteLock", "Removed site lock file " + siteLockFile);
            siteLockFile = null;
        }
    }
}
